Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | /** * API routes for managing custom voice recordings. * * POST — Upload a recorded clip (webm blob via FormData) * PATCH — Deactivate or reactivate a clip * DELETE — Permanently remove a clip */ import { mkdir, rename, unlink, stat } from 'fs/promises' import { NextResponse } from 'next/server' import { join } from 'path' import { withAuth } from '@/lib/auth/withAuth' const AUDIO_DIR = join(process.cwd(), 'data', 'audio') function validateSegment(segment: string): boolean { return !!segment && !segment.includes('/') && !segment.includes('..') && !segment.includes('\0') } /** * POST /api/admin/audio/custom-clips/[voice]/[clipId] * * Upload a recorded audio clip. Expects multipart FormData with an `audio` field. */ export const POST = withAuth( async (request, { params }) => { try { const { voice, clipId } = (await params) as { voice: string; clipId: string } if (!validateSegment(voice) || !validateSegment(clipId)) { return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 }) } const formData = await request.formData() const audioBlob = formData.get('audio') if (!audioBlob || !(audioBlob instanceof Blob)) { return NextResponse.json({ error: 'Missing audio blob in form data' }, { status: 400 }) } const voiceDir = join(AUDIO_DIR, voice) await mkdir(voiceDir, { recursive: true }) const buffer = Buffer.from(await audioBlob.arrayBuffer()) const filePath = join(voiceDir, `cc-${clipId}.webm`) const { writeFile } = await import('fs/promises') await writeFile(filePath, buffer) return NextResponse.json({ ok: true }) } catch (error) { console.error('Error uploading custom clip:', error) return NextResponse.json({ error: 'Failed to upload clip' }, { status: 500 }) } }, { role: 'admin' } ) /** * PATCH /api/admin/audio/custom-clips/[voice]/[clipId] * * Deactivate or reactivate a clip. * Body: { action: 'deactivate' | 'reactivate' } */ export const PATCH = withAuth( async (request, { params }) => { try { const { voice, clipId } = (await params) as { voice: string; clipId: string } if (!validateSegment(voice) || !validateSegment(clipId)) { return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 }) } const body = await request.json() const { action } = body as { action: string } if (action !== 'deactivate' && action !== 'reactivate') { return NextResponse.json( { error: 'action must be "deactivate" or "reactivate"' }, { status: 400 } ) } const voiceDir = join(AUDIO_DIR, voice) const deactivatedDir = join(voiceDir, '.deactivated') // Try both .webm and .mp3 const extensions = ['.webm', '.mp3'] for (const ext of extensions) { const activePath = join(voiceDir, `cc-${clipId}${ext}`) const deactivatedPath = join(deactivatedDir, `cc-${clipId}${ext}`) if (action === 'deactivate') { try { await stat(activePath) await mkdir(deactivatedDir, { recursive: true }) await rename(activePath, deactivatedPath) } catch { // File doesn't exist in this extension, skip } } else { try { await stat(deactivatedPath) await rename(deactivatedPath, activePath) } catch { // File doesn't exist in this extension, skip } } } return NextResponse.json({ ok: true }) } catch (error) { console.error('Error patching custom clip:', error) return NextResponse.json({ error: 'Failed to update clip' }, { status: 500 }) } }, { role: 'admin' } ) /** * DELETE /api/admin/audio/custom-clips/[voice]/[clipId] * * Permanently remove a clip from both active and deactivated locations. */ export const DELETE = withAuth( async (_request, { params }) => { try { const { voice, clipId } = (await params) as { voice: string; clipId: string } if (!validateSegment(voice) || !validateSegment(clipId)) { return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 }) } const voiceDir = join(AUDIO_DIR, voice) const deactivatedDir = join(voiceDir, '.deactivated') const extensions = ['.webm', '.mp3'] const prefixes = ['cc-', ''] for (const ext of extensions) { for (const prefix of prefixes) { for (const dir of [voiceDir, deactivatedDir]) { try { await unlink(join(dir, `${prefix}${clipId}${ext}`)) } catch { // File doesn't exist, skip } } } } return NextResponse.json({ ok: true }) } catch (error) { console.error('Error deleting custom clip:', error) return NextResponse.json({ error: 'Failed to delete clip' }, { status: 500 }) } }, { role: 'admin' } ) |